package k8s

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"text/template"

	"gopkg.openfuyao.cn/bkecommon"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/uuid"
	yamlutil "k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"

	"gopkg.openfuyao.cn/bkeadm/utils"
	"gopkg.openfuyao.cn/bkeadm/utils/log"
)

type KubernetesClient interface {
	GetClient() kubernetes.Interface
	GetDynamicClient() dynamic.Interface
	InstallYaml(filename string, variable map[string]string, ns string) error
	PatchYaml(filename string, variable map[string]string) error
	UninstallYaml(filename string, ns string) error
	WatchEventByAnnotation(namespace string)
	CreateNamespace(namespace *corev1.Namespace) error
	CreateSecret(secret *corev1.Secret) error
	GetNamespace(filename string) (string, error)
}

type Client struct {
	ClientSet     *kubernetes.Clientset
	DynamicClient dynamic.Interface
}

func NewKubernetesClient(kubeConfig string) (KubernetesClient, error) {
	var config *rest.Config
	var err error

	if kubeConfig == "" {
		if home := homedir.HomeDir(); home != "" {
			kubeConfig = filepath.Join(home, ".kube", "config")
		}
	}
	if utils.Exists(kubeConfig) {
		config, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
	}
	if err != nil {
		return nil, err
	}
	if config == nil {
		return nil, errors.New("The kube config configuration file does not exist. ")
	}

	clientSet, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Infof("Failed to initialize kubernetes clientset")
		return nil, err
	}

	dynamicClient, err := dynamic.NewForConfig(config)
	if err != nil {
		log.Infof("Failed to initialize kubernetes dynamic client")
		return nil, err
	}

	return &Client{
		ClientSet:     clientSet,
		DynamicClient: dynamicClient,
	}, nil
}

func (c *Client) GetClient() kubernetes.Interface {
	return c.ClientSet
}

func (c *Client) GetDynamicClient() dynamic.Interface {
	return c.DynamicClient
}

func (c *Client) InstallYaml(filename string, variable map[string]string, ns string) error {
	// Replace template variables
	tmpl, err := template.ParseFiles(filename)
	if err != nil {
		return err
	}
	tmpFile := fmt.Sprintf("/tmp/bke_%s.yaml", uuid.NewUUID())
	file, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY, 0644)
	defer func() {
		_ = os.Remove(tmpFile)
	}()

	err = tmpl.Execute(file, variable)
	if err != nil {
		return err
	}

	// Apply resources to the K8S cluster
	f, err := os.Open(tmpFile)
	defer func() {
		_ = f.Close()
	}()

	decoder := yamlutil.NewYAMLOrJSONDecoder(f, 4096)
	dc := c.ClientSet.Discovery()
	restMapperRes, err := restmapper.GetAPIGroupResources(dc)
	if err != nil {
		return err
	}
	restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)

	for {
		var rawObj runtime.RawExtension
		if err = decoder.Decode(&rawObj); err != nil {
			if err == io.EOF {
				break
			} else {
				return err
			}
		}
		// runtime.Object
		obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(rawObj.Raw, nil, nil)
		if err != nil {
			return err
		}
		mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			return err
		}
		// runtime.Object convert to unstructured
		unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
		if err != nil {
			return err
		}
		unstruct := &unstructured.Unstructured{Object: unstructuredObj}

		var obj2 *unstructured.Unstructured
		if ns == "" && unstruct.GetNamespace() == "" {
			obj2, err = c.DynamicClient.Resource(mapping.Resource).Create(context.Background(), unstruct, metav1.CreateOptions{})
			if err != nil {
				if !apierrors.IsAlreadyExists(err) {
					return err
				}
				log.Debugf("%s/%s already exists", unstruct.GetKind(), unstruct.GetName())
				continue
			}
			log.Debugf("%s/%s created", obj2.GetKind(), obj2.GetName())
		} else {
			newNs := unstruct.GetNamespace()
			if ns != "" {
				newNs = ns
			}
			obj2, err = c.DynamicClient.Resource(mapping.Resource).Namespace(newNs).Create(context.Background(), unstruct, metav1.CreateOptions{})
			if err != nil {
				if !apierrors.IsAlreadyExists(err) {
					return err
				}
				log.Debugf("%s/%s already exists", unstruct.GetKind(), unstruct.GetName())
				continue
			}
			log.Debugf("%s/%s created", obj2.GetKind(), obj2.GetName())
		}
	}
	return nil
}

func (c *Client) PatchYaml(filename string, variable map[string]string) error {
	// Replace template variables
	tmpl, err := template.ParseFiles(filename)
	if err != nil {
		return err
	}
	tmpFile := fmt.Sprintf("/tmp/bke_%s.yaml", uuid.NewUUID())
	file, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY, 0644)
	defer func() {
		_ = os.Remove(tmpFile)
	}()

	err = tmpl.Execute(file, variable)
	if err != nil {
		return err
	}

	// Apply resources to the K8S cluster
	f, err := os.Open(tmpFile)
	defer func() {
		_ = f.Close()
	}()

	decoder := yamlutil.NewYAMLOrJSONDecoder(f, 4096)
	dc := c.ClientSet.Discovery()
	restMapperRes, err := restmapper.GetAPIGroupResources(dc)
	if err != nil {
		return err
	}
	restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)

	for {
		var rawObj runtime.RawExtension
		if err = decoder.Decode(&rawObj); err != nil {
			if err == io.EOF {
				break
			} else {
				return err
			}
		}
		// runtime.Object
		obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(rawObj.Raw, nil, nil)
		if err != nil {
			return err
		}
		mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			return err
		}
		// runtime.Object convert to unstructured
		unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
		if err != nil {
			return err
		}
		unstruct := &unstructured.Unstructured{Object: unstructuredObj}
		json, err := unstruct.MarshalJSON()
		if err != nil {
			return err
		}
		obj, err = c.DynamicClient.Resource(mapping.Resource).Namespace(unstruct.GetNamespace()).Patch(context.Background(), unstruct.GetName(), types.MergePatchType, json, metav1.PatchOptions{})
		if err != nil {
			log.Errorf("failed patch %v", gvk)
			return err
		}
	}
	return nil
}

func (c *Client) UninstallYaml(filename string, ns string) error {

	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	decoder := yamlutil.NewYAMLOrJSONDecoder(f, 4096)
	dc := c.ClientSet.Discovery()
	restMapperRes, err := restmapper.GetAPIGroupResources(dc)
	if err != nil {
		return err
	}
	restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)

	for {
		rawObj := runtime.RawExtension{}
		if err := decoder.Decode(&rawObj); err != nil {
			if err == io.EOF {
				break
			} else {
				return err
			}
		}

		// runtime.Object
		obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(rawObj.Raw, nil, nil)
		if err != nil {
			return err
		}

		mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			return err
		}

		// runtime.Object convert to unstructured
		unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
		if err != nil {
			return err
		}

		tmpMetadata := unstructuredObj["metadata"].(map[string]interface{})
		tmpName := tmpMetadata["name"].(string)
		tmpNameSpace := tmpMetadata["namespace"].(string)

		if ns == "" {
			err := c.DynamicClient.Resource(mapping.Resource).Namespace(tmpNameSpace).Delete(context.Background(), tmpName, metav1.DeleteOptions{})
			if err != nil {
				return err
			}
		} else {
			err := c.DynamicClient.Resource(mapping.Resource).Namespace(ns).Delete(context.Background(), tmpName, metav1.DeleteOptions{})
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// Listen for cluster deployment events

func (c *Client) WatchEventByAnnotation(namespace string) {
	stop := make(chan struct{})
	defer func() {
		_, isOpen := <-stop
		if isOpen {
			close(stop)
		}
	}()

	clientSet := c.GetClient()
	watchList := &cache.ListWatch{
		ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
			ls, err := clientSet.CoreV1().Events(namespace).List(context.Background(), options)
			return ls, err
		},
		WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
			w, err := clientSet.CoreV1().Events(namespace).Watch(context.Background(), options)
			return w, err
		},
	}
	eventHandler := cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			e, ok := obj.(*corev1.Event)
			if ok {
				if e.Annotations == nil {
					return
				}
				if _, ok = e.Annotations[bkecommon.BKEFinishEventAnnotationKey]; ok {
					log.BKEFormat(e.Type, fmt.Sprintf("Reason: %s, Message: %s", e.Reason, e.Message))
					if !utils.IsChanClosed(stop) {
						close(stop)
					}
					return
				}
				if _, ok = e.Annotations[bkecommon.BKEEventAnnotationKey]; ok {
					log.BKEFormat(e.Type, fmt.Sprintf("Reason: %s, Message: %s", e.Reason, e.Message))
				}
			}
		},
		DeleteFunc: func(obj interface{}) {
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
		},
	}

	_, controller := cache.NewInformer(
		watchList,
		&corev1.Event{},
		0,
		eventHandler,
	)
	controller.Run(stop)
}

func (c *Client) CreateNamespace(namespace *corev1.Namespace) error {
	_, err := c.ClientSet.CoreV1().Namespaces().Get(context.Background(), namespace.Name, metav1.GetOptions{})
	if err != nil {
		_, err = c.ClientSet.CoreV1().Namespaces().Create(context.Background(), namespace, metav1.CreateOptions{})
	}
	return err
}

func (c *Client) CreateSecret(secret *corev1.Secret) error {
	_, err := c.ClientSet.CoreV1().Secrets(secret.Namespace).Get(context.Background(), secret.Name, metav1.GetOptions{})
	if err != nil {
		_, err = c.ClientSet.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
	}
	return err
}

func (c *Client) GetNamespace(filename string) (string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	decoder := yamlutil.NewYAMLOrJSONDecoder(f, 4096)
	for {
		rawObj := runtime.RawExtension{}
		if err := decoder.Decode(&rawObj); err != nil {
			if err == io.EOF {
				break
			} else {
				return "", err
			}
		}

		// runtime.Object
		obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(rawObj.Raw, nil, nil)
		if err != nil {
			return "", err
		}
		if gvk != nil && gvk.Version == "v1" && gvk.Kind == "Namespace" {
			// runtime.Object convert to unstructured
			unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
			if err != nil {
				return "", err
			}
			tmpMetadata := unstructuredObj["metadata"].(map[string]interface{})
			name := tmpMetadata["name"].(string)
			return name, nil
		}
	}
	return "", nil
}
